﻿using DaVikingCode.RectanglePacking;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace DaVikingCode.AssetPacker
{
    public struct SpriteInfo
    {
        /// <summary>
        /// 小图在图集中四个顶点对应的uv信息
        /// </summary>
        public Vector2[] uvs;
        /// <summary>
        /// 小图精灵
        /// </summary>
        public Sprite sprite;

        /// <summary>
        /// 小精灵信息
        /// </summary>
        /// <param name="_sp">对应精灵</param>
        /// <param name="textureSize">图集大小</param>
        public SpriteInfo(Sprite _sp, float textureSize)
        {
            sprite = _sp;

            uvs = new Vector2[4];
            uvs[0] = new Vector2((sprite.rect.x / textureSize), (sprite.rect.y / textureSize));
            uvs[1] = new Vector2((sprite.rect.x / textureSize), ((sprite.rect.y + sprite.rect.height) / textureSize));
            uvs[2] = new Vector2(((sprite.rect.x + sprite.rect.width) / textureSize), ((sprite.rect.y + sprite.rect.height) / textureSize));
            uvs[3] = new Vector2(((sprite.rect.x + sprite.rect.width) / textureSize), (sprite.rect.y / textureSize));
        }
    }

	public class AssetPacker
    {
        private sealed class UnityMono : MonoBehaviour
        {
            public static UnityMono Instance;
            void Awake()
            {
                Instance = this;
            }
        }

        public delegate void onCompleted(bool isSuccess, string msg);
        public event onCompleted OnProcessCompleted;

		public float pixelsPerUnit = 100.0f;

		public bool useCache = false;
		public string cacheName = "";
		public int cacheVersion = 1;
		public bool deletePreviousCacheVersion = true;

		protected Dictionary<string, Sprite> mSprites = new Dictionary<string, Sprite>();
		protected List<TextureToPack> itemsToRaster = new List<TextureToPack>();
        
        private int targetTextureSize = 1024;//目标图集尺寸
        public int TargetTextureSize
        {
            get { return this.targetTextureSize; }
            private set { this.targetTextureSize = value; }
        }
        private Texture2D firstTexture;
        /// <summary>
        /// 生成的第一张纹理图像
        /// </summary>
        public Texture2D FirstTexture
        {
            get;
            private set;
        }

		public void AddTextureToPack(string file, string customID = null) {

			itemsToRaster.Add(new TextureToPack(file, customID != null ? customID : Path.GetFileNameWithoutExtension(file)));
		}
        
		public void AddTexturesToPack(string[] files) {

			foreach (string file in files)
				AddTextureToPack(file);
		}

        /// <summary>
        /// 生成图集
        /// </summary>
        /// <param name="textureSize">目标图集尺寸</param>
		public void Process(string fileName, int textureSize = 1024) {
            if(null == GameObject.Find("AssetPacker"))
            {
                var _go = new GameObject("AssetPacker");
                _go.AddComponent<UnityMono>();
            }

            this.targetTextureSize = textureSize;
			if (useCache) {

				if (cacheName == "")
					throw new Exception("No cache name specified");

				string path = Application.persistentDataPath + "/AssetPacker/" + cacheName + "/" + cacheVersion + "/";

				bool cacheExist = Directory.Exists(path);

				if (!cacheExist)
                    UnityMono.Instance.StartCoroutine(createPack(fileName, path));
				else
                    UnityMono.Instance.StartCoroutine(loadPack(fileName, path));
				
			} else
                UnityMono.Instance.StartCoroutine(createPack(fileName));
		}

		protected IEnumerator createPack(string fileName, string savePath = "") {

			if (savePath != "") {

				if (deletePreviousCacheVersion && Directory.Exists(Application.persistentDataPath + "/AssetPacker/" + cacheName + "/"))
					foreach (string dirPath in Directory.GetDirectories(Application.persistentDataPath + "/AssetPacker/" + cacheName + "/", "*", SearchOption.AllDirectories))
						Directory.Delete(dirPath, true);

				Directory.CreateDirectory(savePath);
			}

			List<Texture2D> textures = new List<Texture2D>();
			List<string> images = new List<string>();

			foreach (TextureToPack itemToRaster in itemsToRaster) {

				WWW loader = new WWW("file:///" + itemToRaster.file);

				yield return loader;

				textures.Add(loader.texture);
				images.Add(itemToRaster.id);
			}

            this._pack(textures, images, fileName, savePath);
		}

        private void _pack(List<Texture2D> textures, List<string> images, string fileName, string savePath = "")
        {
            int textureSize = this.targetTextureSize;

            List<Rect> rectangles = new List<Rect>();
            for (int i = 0; i < textures.Count; i++)
                if (textures[i].width > textureSize || textures[i].height > textureSize)
                    OnProcessCompleted.Invoke(false, "A texture size is bigger than the sprite sheet size!");
                else
                    rectangles.Add(new Rect(0, 0, textures[i].width, textures[i].height));

            if(0 == rectangles.Count)
            {
                OnProcessCompleted.Invoke(false, "rectangles.Count = 0");
                return;
            }

            const int padding = 1;
            int numSpriteSheet = 0;
            while (rectangles.Count > 0)
            {
                //format格式必须为未压缩的非HDR格式，原因：https://docs.unity3d.com/ScriptReference/ImageConversion.EncodeToPNG.html
                //texture.EncodeToPNG()： This function works only on uncompressed, non-HDR texture formats. The texture also has to have Is Readable flag set in the import settings.
                //TexturFormat限制：SetPixels32() --> This function works only on RGBA32, ARGB32 texture formats. For other formats SetPixels32 is ignored. The texture also has to have Is Readable flag set in the import settings.
                TextureFormat format = TextureFormat.ARGB32;//综上，因此不可使用andorid普遍的ETC格式与iOS的PVRTC格式

                Texture2D texture = new Texture2D(textureSize, textureSize, format, false);
                Color32[] fillColor = texture.GetPixels32();
                for (int i = 0; i < fillColor.Length; ++i)
                    fillColor[i] = Color.clear;

                RectanglePacker packer = new RectanglePacker(texture.width, texture.height, padding);

                for (int i = 0; i < rectangles.Count; i++)
                    packer.insertRectangle((int)rectangles[i].width, (int)rectangles[i].height, i);

                packer.packRectangles();

                if (packer.rectangleCount > 0)
                {
                    texture.SetPixels32(fillColor);

                    IntegerRectangle rect = new IntegerRectangle();
                    List<TextureAsset> textureAssets = new List<TextureAsset>();

                    List<Rect> garbageRect = new List<Rect>();
                    List<Texture2D> garabeTextures = new List<Texture2D>();
                    List<string> garbageImages = new List<string>();

                    for (int j = 0; j < packer.rectangleCount; j++)
                    {
                        rect = packer.getRectangle(j, rect);

                        int index = packer.getRectangleId(j);

                        texture.SetPixels32(rect.x, rect.y, rect.width, rect.height, textures[index].GetPixels32());

                        TextureAsset textureAsset = new TextureAsset();
                        textureAsset.x = rect.x;
                        textureAsset.y = rect.y;
                        textureAsset.width = rect.width;
                        textureAsset.height = rect.height;
                        textureAsset.name = images[index];

                        textureAssets.Add(textureAsset);

                        garbageRect.Add(rectangles[index]);
                        garabeTextures.Add(textures[index]);
                        garbageImages.Add(images[index]);
                    }

                    foreach (Rect garbage in garbageRect)
                        rectangles.Remove(garbage);

                    foreach (Texture2D garbage in garabeTextures)
                        textures.Remove(garbage);

                    foreach (string garbage in garbageImages)
                        images.Remove(garbage);

                    texture.Apply();

                    if (savePath != "")
                    {
                        File.WriteAllBytes(savePath + "/" + fileName + numSpriteSheet + ".png", texture.EncodeToPNG());
                        File.WriteAllText(savePath + "/" + fileName + numSpriteSheet + ".json", JsonUtility.ToJson(new TextureAssets(textureAssets.ToArray())));
                        ++numSpriteSheet;
                    }

                    if(0 == numSpriteSheet)
                    {
                        this.FirstTexture = texture;
                    }

                    foreach (TextureAsset textureAsset in textureAssets)
                        mSprites.Add(textureAsset.name, Sprite.Create(texture, new Rect(textureAsset.x, textureAsset.y, textureAsset.width, textureAsset.height), Vector2.zero, pixelsPerUnit, 0, SpriteMeshType.FullRect));
                }

            }

            OnProcessCompleted.Invoke(true, "createPack ok");
        }

		protected IEnumerator loadPack(string fileName, string savePath) {
			
			int numFiles = Directory.GetFiles(savePath).Length;

			for (int i = 0; i < numFiles / 2; ++i) {

				WWW loaderTexture = new WWW("file:///" + savePath + "/" + fileName + i + ".png");
				yield return loaderTexture;

				WWW loaderJSON = new WWW("file:///" + savePath + "/" + fileName + i + ".json");
				yield return loaderJSON;

				TextureAssets textureAssets = JsonUtility.FromJson<TextureAssets> (loaderJSON.text);

				Texture2D t = loaderTexture.texture; // prevent creating a new Texture2D each time.

                if (0 == i)
                {
                    this.FirstTexture = t;
                }
                foreach (TextureAsset textureAsset in textureAssets.assets)
					mSprites.Add(textureAsset.name, Sprite.Create(t, new Rect(textureAsset.x, textureAsset.y, textureAsset.width, textureAsset.height), Vector2.zero, pixelsPerUnit, 0, SpriteMeshType.FullRect));
			}

			yield return null;

			OnProcessCompleted.Invoke(true, "loadPack ok");
		}

		public void Dispose() {

			foreach (var asset in mSprites)
                UnityEngine.Object.Destroy(asset.Value.texture);

			mSprites.Clear();
		}

		void Destroy() {

			Dispose();
		}

		public SpriteInfo GetSprite(string id) {

			Sprite sprite = null;

			mSprites.TryGetValue (id, out sprite);

			return new SpriteInfo(sprite, this.TargetTextureSize);
		}

		public SpriteInfo[] GetSprites(string prefix) {

			List<string> spriteNames = new List<string>();
			foreach (var asset in mSprites)
				if (asset.Key.StartsWith(prefix))
					spriteNames.Add(asset.Key);

			spriteNames.Sort(StringComparer.Ordinal);

			List<SpriteInfo> sprites = new List<SpriteInfo>();
			Sprite sprite;
			for (int i = 0; i < spriteNames.Count; ++i) {

				mSprites.TryGetValue(spriteNames[i], out sprite);

				sprites.Add(new SpriteInfo(sprite, this.TargetTextureSize));
			}

			return sprites.ToArray();
		}
	}
}
